﻿#region ... Copyright Notice ...
/*
   Copyright 2008 Tyler Jensen

	Author: Tyler Jensen    http://www.tsjensen.com/blog

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
#endregion

using System;
using System.Data;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using Atrax.Contracts;
using Atrax.Library;
using Atrax.Logging;
using Atrax.Utils;

namespace Atrax.Master
{
	internal static class QueryController
	{
		static object locker = new object();
		static SortedDictionary<string, IQueryProcessor> queryProcessors = new SortedDictionary<string, IQueryProcessor>(new DescendingStringComparer());
		static Dictionary<string, IQueryProcessor> queryWorkerPool = new Dictionary<string, IQueryProcessor>();

		static int workerPoolSize = 0;
		static int maxSecondsInWorkerPool = 0;
		static Dictionary<string, DateTime> queryWorkerPoolTimeIn = new Dictionary<string, DateTime>();

		static Dictionary<string, RequestHandler> handlers = new Dictionary<string, RequestHandler>();

		static string currentStatusCode = "OK";
		static string currentStatusDescription = "All okay.";

		static int availableIpAddressCount = 0;

		//static SortedDictionary<string, QueryRequestData> queryProcessors = new SortedDictionary<string, QueryRequestData>(new DescendingStringComparer());
		//static Dictionary<string, QueryRequestData> m_queryWorkerPool = new Dictionary<string, QueryRequestData>();
		//static Dictionary<string, RequestHandler> m_handlers = new Dictionary<string, RequestHandler>();
		//static List<long> m_AuthorizedUserKeys = new List<long>();
		//static int m_workerPoolSize = 0;
		//static int m_maxSecondsInWorkerPool = 0;
		//static DateTime m_started = DateTime.MinValue;
		//static Dictionary<QueryType, int> m_querysCompleted = new Dictionary<QueryType,int>();

		internal static QueryResponse EnqueRequest(Query query)
		{
			lock (locker)
			{
				if (availableIpAddressCount == 0) availableIpAddressCount = GetAvailableIpAddressCount();
				QueryResponse response = null;
				try
				{
					//1. assigne queryId
					query.Header.QueryId = GenerateQueryId();

					//2. load query processor and call initialize 
					IQueryProcessor processor = ProcessorFactory.Create(query.Header.QueryType);
					if (processor == null)
					{
						response = new QueryResponse() 
						{ 
							QueryId = query.Header.QueryId, 
							StatusCode = "561", 
							StatusDescription = "Unable to load query processor.", 
							ValidationResult = new QueryValidationResult() 
							{ 
								IsValid = false, 
								ValidationReport = string.Empty 
							} 
						};
						return response;
					}
					processor.Initialize(query);
					
					//3. call validation and return if invalid
					QueryValidationResult qvr = processor.Validate();
					if (!qvr.IsValid)
					{
						response = new QueryResponse()
						{
							QueryId = processor.Query.Header.QueryId,
							StatusCode = "563",
							StatusDescription = "Invalid query.",
							ValidationResult = qvr, 
							EstimatedDeliverySeconds = processor.GetProcessingTimeEstimate(availableIpAddressCount)
						};
						return response;
					}
					
					//4. valid, so add query and processor to list
					query.Header.ProcessingAttempts++;  //increment this attempt before persisting to allow processor to 
					PersistenceManager.SaveQuery(query);  //save the query to the app_data folder
					queryProcessors.Add(processor.Query.Header.QueryId, processor);  //add query to sorted list for processing
					ManageWorkInProgress(); //kicks off the query if none are in the "worker pool"

					response = new QueryResponse()
					{
						QueryId = processor.Query.Header.QueryId,
						StatusCode = "200",
						StatusDescription = "OK",
						ValidationResult = qvr
					};
					return response;
				}
				catch (Exception e)
				{
					Log.App(LogEvents.RequestControllerEnqueueRequestFailed,
						"queryid=" + query.Header.QueryId + " | querytype=" + query.Header.QueryType + " | licensekey=" + query.Header.LicenseKey, e);
				}
				response = new QueryResponse()
				{
					QueryId = query.Header.QueryId,
					StatusCode = "565",
					StatusDescription = "Unknown error occurred in evaluating query.",
					ValidationResult = new QueryValidationResult()
					{
						IsValid = false,
						ValidationReport = string.Empty
					}
				};
				return response;
			}
		}

		static int GetAvailableIpAddressCount()
		{
			return Convert.ToInt32(ConfigurationManager.AppSettings["availableIpAddressCount"]);
		}

		private static string GenerateQueryId()
		{
			return DateTime.UtcNow.ToString("yyyyMMdd_hhmmssfff") + "_" + Guid.NewGuid().ToString();
		}

		/// <summary>
		/// Remove query from worker pool and query list. Assumes query is complete and response callback has been sent.
		/// </summary>
		/// <param name="queryId"></param>
		internal static void CompleteQuery(string queryId)
		{
			lock (locker)
			{
				try
				{
					if (queryWorkerPool.ContainsKey(queryId))
					{
						queryWorkerPool.Remove(queryId);
					}
					if (queryProcessors.ContainsKey(queryId))
					{
						queryProcessors.Remove(queryId);
					}
					if (queryWorkerPoolTimeIn.ContainsKey(queryId)) queryWorkerPoolTimeIn.Remove(queryId);
					if (handlers.ContainsKey(queryId)) handlers.Remove(queryId);
					PersistenceManager.Remove(queryId);

					ManageWorkInProgress(); //called to setup next query in line if any
				}
				catch (Exception e)
				{
					Log.App(LogEvents.RequestControllerCompletedQueryFailed, "queryid=" + queryId, e);
				}
			}
		}

		/// <summary>
		/// Used to fetch a reference to the request handler for followups. Called by the callback processor.
		/// </summary>
		/// <param name="queryId"></param>
		/// <returns></returns>
		internal static RequestHandler GetRequestHandler(string queryId)
		{
			lock (locker)
			{
				if (handlers.ContainsKey(queryId))
					return handlers[queryId];
				else
					return null;
			}
		}

		/// <summary>
		/// Returns list of current querys. Used by query management interface.
		/// </summary>
		/// <returns></returns>
		internal static MasterStatus GetStatusReport()
		{
			lock (locker)
			{
				//todo: make this more detailed and useful
				List<string> idsInQueue = new List<string>();
				if (availableIpAddressCount == 0) availableIpAddressCount = GetAvailableIpAddressCount();
				double deliverySecondsTotal = 0.0;
				foreach(KeyValuePair<string, RequestHandler> pair in handlers)
				{
					deliverySecondsTotal += pair.Value.Processor.GetProcessingTimeEstimate(availableIpAddressCount);
					idsInQueue.Add(pair.Key);
				}
				MasterStatus report = new MasterStatus()
				{ 
					EstimatedDeliverySeconds = deliverySecondsTotal,
					QueryIdsInQueue = idsInQueue.ToArray(), 
					StatusCode = currentStatusCode, 
					StatusDescription = currentStatusDescription 
				};
				return report;
			}
		}

		/// <summary>
		/// Used to remove a query from processing regardless of its progress. Used by query management interface.
		/// </summary>
		/// <param name="queryId"></param>
		internal static void KillQuery(string queryId)
		{
			CompleteQuery(queryId); //same thing - removes it from lists and may or may not survive
		}

		//todo: implement a ChangeQueryPriority

		/// <summary>
		/// Only called by the IIS startup in case the process was recycled.
		/// </summary>
		internal static void LoadFromPersistence()
		{
			lock (locker)
			{
				List<Query> querys = PersistenceManager.LoadQuerys();
				if (querys.Count > 0)
				{
					queryWorkerPool.Clear();
					queryProcessors.Clear();
					queryWorkerPoolTimeIn.Clear();
					handlers.Clear();
					foreach (Query query in querys)
					{
						IQueryProcessor processor = ProcessorFactory.Create(query.Header.QueryType);
						processor.Initialize(query);
						query.Header.ProcessingAttempts++;  //increment this attempt before persisting to allow processor to 
						PersistenceManager.SaveQuery(query);  //save the query to the app_data folder
						queryProcessors.Add(processor.Query.Header.QueryId, processor);  //add query to sorted list for processing
					}
					ManageWorkInProgress();
				}
			}
		}

		static void ManageWorkInProgress()
		{
			try
			{
				//initialize worker pool and max seconds if needed
				if (workerPoolSize == 0)
				{
					workerPoolSize = Convert.ToInt32(ConfigurationManager.AppSettings["workerPoolSize"]);
					maxSecondsInWorkerPool = Convert.ToInt32(ConfigurationManager.AppSettings["maxSecondsInWorkerPool"]);
				}

				//check to see if any worker pool items have expired using the queryWorkerPoolTimeIn collection
				//find expired querys
				List<string> expiredQuerys = new List<string>();
				List<string> nearlyExpiredQuerys = new List<string>();
				foreach (KeyValuePair<string, DateTime> item in queryWorkerPoolTimeIn)
				{
					TimeSpan ts = DateTime.Now - item.Value;
					if (ts.TotalSeconds > maxSecondsInWorkerPool) expiredQuerys.Add(item.Key);
					if (ts.TotalSeconds > (maxSecondsInWorkerPool / 2)) nearlyExpiredQuerys.Add(item.Key); //has gone beyond 1/2 time allowed
				}
				//remove expired querys from collections and removed persistence layer -- notification sent of expiration
				foreach (string id in expiredQuerys)
				{
					IQueryProcessor exquery = queryProcessors[id];
					if (queryWorkerPool.ContainsKey(id)) queryWorkerPool.Remove(id);
					if (queryProcessors.ContainsKey(id)) queryProcessors.Remove(id);
					if (queryWorkerPoolTimeIn.ContainsKey(id)) queryWorkerPoolTimeIn.Remove(id);
					if (handlers.ContainsKey(id)) handlers.Remove(id);
					PersistenceManager.Remove(id);  //permanently remove query
					//log expired query
					Log.App(LogEvents.RequestControllerExpiredQuery, "queryid=" + exquery.Query.Header.QueryId + " | querytype=" + exquery.Query.Header.QueryType + " | licensekey=" + exquery.Query.Header.LicenseKey);
					Emailer em = new Emailer();
					em.SendQueryExpiredNotification(exquery.Query); //send notification
				}
				//ping nearly expired querys to see if they can resurrect themselves
				foreach (string id in nearlyExpiredQuerys)
				{
					if (handlers.ContainsKey(id))
					{
						try
						{
							handlers[id].PingNearlyExpired();
						}
						catch (Exception e)
						{
							Log.App(LogEvents.RequestControllerPingNearlyExpiredFailed, "queryid=" + id, e);
						}
					}
				}

				//checks to see if a query should be put into the m_queryWorkerPool collection and added to the managed thread pool
				//pulls items by priority from queryProcessors to add to m_queryWorkerPool if not full and gets their RequestHandler from the RequestHandlerFactory
				//add adds the requestHandler to the managed thread pool
				if (queryWorkerPool.Count < workerPoolSize)
				{
					int itemsToAdd = workerPoolSize - queryWorkerPool.Count;
					foreach (KeyValuePair<string, IQueryProcessor> item in queryProcessors)
					{
						if (itemsToAdd == 0) break;
						if (!queryWorkerPool.ContainsKey(item.Key)) //only add if not in there
						{
							queryWorkerPool.Add(item.Key, item.Value);
							if (queryWorkerPoolTimeIn.ContainsKey(item.Key)) queryWorkerPoolTimeIn.Remove(item.Key); //remove if already there
							queryWorkerPoolTimeIn.Add(item.Key, DateTime.Now); //set start time of this work item
							RequestHandler rh = new RequestHandler();
							handlers.Add(item.Key, rh);
							ManagedThreadPool.QueueUserWorkItem(rh.ThreadPoolCallback, item.Value);
							itemsToAdd--; //decrement
						}
					}
				}
			}
			catch (Exception e)
			{
				Log.App(LogEvents.RequestControllerManageWorkInProgressFailed, e);
			}
		}
	}

	internal class DescendingStringComparer : IComparer<string>
	{
		public int Compare(string x, string y)
		{
			return string.Compare(y, x, true);  //by reversing the params, we get descending order
		}
	}
}
